package com.cis467.mrball;

import java.util.Random;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;	
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;

public class MrBallView extends SurfaceView implements SurfaceHolder.Callback {
	class MrBallThread extends Thread implements SensorEventListener{
       
        /*
         * Keys for saving state
         */
        private static final String KEY_X = "mX";
        private static final String KEY_Y = "mY";
        private static final String KEY_DX = "mDX";
        private static final String KEY_DY = "mDY";
        
		 /*
         * State-tracking constants
         */
        public static final int STATE_PAUSE = 1;
        public static final int STATE_READY = 2;
        public static final int STATE_RUNNING = 3;
        public static final int STATE_WIN = 4;
		
        /*
         * Dimensions for the game arrays
         */
        private static final int FLOOR_COLS = 15;
        private static final int FLOOR_ROWS = 10;
        private static final int VWALL_COLS = FLOOR_COLS + 1;
        private static final int VWALL_ROWS = FLOOR_ROWS;
        private static final int HWALL_COLS = FLOOR_COLS; 
        private static final int HWALL_ROWS = FLOOR_ROWS + 1;
        
        /*
         * Floor Types
         */
        private static final int FLOOR_PIT = 0;
        private static final int FLOOR_SLOW = 1;
        private static final int FLOOR_NORMAL = 2;
        private static final int FLOOR_FAST = 3;
 //       private static final int FLOOR_PORTAL = 4;
        
        /*
         * Wall Types
         */
        private static final int WALL_NONE = 0;
        private static final int WALL_NORMAL = 1;
        private static final int WALL_DOOR = 2;
        private static final int WALL_LASER = 3;
        
        /*
         * Game play constants
         */
        
        private static final double BALL_RADIUS = 4.0;
        private static final double TILE_SIZE = 10.0;
        private static final double BOARD_WIDTH = TILE_SIZE * FLOOR_COLS;
        private static final double BOARD_HEIGHT = TILE_SIZE * FLOOR_ROWS;
        private static final double GRAVITY = 1000.0;
        private static final double MAX_SPEED = 2000.0;
        private static final double BOUNCE = -0.5;
        private static final int ORIENTATION_BACKLOG = 4;
        
        
        /*
         * Layout Constants
         */
        private static final int MIN_SIDE_PANEL_WIDTH = 150;
        
        /*
         * Conversion constant for degrees to radians
         */
        private static final double DEG_TO_RAD = 3.14/180.0;
        
		 /** Handle to the surface manager object we interact with */
        private SurfaceHolder mSurfaceHolder;
        
        /** Message handler used by thread to interact with TextView */
        private Handler mHandler;
        
        private SensorManager mSensorManager;
        
        /**
         * Current height of the screen.
         * 
         * @see #setSurfaceSize
         */
        private int mScreenHeight = 1;

        /**
         * Current width of the screen.
         * 
         * @see #setSurfaceSize
         */
        private int mScreenWidth = 1;
        
        /**
         * Current height of the board.
         * 
         * @see #setSurfaceSize
         */
        private int mBoardHeight = 1;

        /**
         * Current width of the board.
         * 
         * @see #setSurfaceSize
         */
        private int mBoardWidth = 1;
        
        /**
         * Current height of the offset.
         * 
         * @see #setSurfaceSize
         */
        private int mOffsetHeight = 1;

        /**
         * Current width of the offset.
         * 
         * @see #setSurfaceSize
         */
        private int mOffsetWidth = 1;
        
        /**
         * Current width of the offset.
         * 
         * @see #setSurfaceSize
         */
        private int mSidePanelWidth = 1;
         
        private int mTileSize = 1;
        
        private double mModelToScreenRatio = 1;
        
        /** The drawable to use as the background of the animation canvas */
        private Bitmap mBackgroundImage;
        
        private Bitmap[] mFloorImages = new Bitmap[4];
        
        /** Pixel radius of ball image. */
        private int mMrBallRadius;
        
        /** What to draw for the Lander in its normal state */
        private Drawable mMrBallImage;
        
        /** Indicate whether the surface has been created & is ready to draw */
        private boolean mRun = false;
        
        /** The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN */
        private int mMode;
        
        /** Used to figure out elapsed time between frames */
        private long mLastTime;
        
        /** X Position (center of ball) */
        private double mXPosition = 0;

        /** Y Position (center of ball) */
        private double mYPosition = 0;
        
        /** Velocity dx. */
        private double mXVelocity = 2;

        /** Velocity dy. */
        private double mYVelocity = 2;
        
        /** Rotation counter */
        private int mRotationCounter = 0;
        
        /** X rotation */
        private double[] mXRotation = new double[ORIENTATION_BACKLOG];
        
        /** Y rotation */
        private double[] mYRotation = new double[ORIENTATION_BACKLOG];
        
        private double mXTare;
        
        private double mYTare;
        
        private int mFloors[][] = new int[FLOOR_COLS][FLOOR_ROWS];
        private int mVWalls[][] = new int[VWALL_COLS][VWALL_ROWS];
        private int mHWalls[][] = new int[HWALL_COLS][HWALL_ROWS];
        
        private int mPortal1Col;
        private int mPortal1Row;
        private int mPortal2Col;
        private int mPortal2Row;
        
		
        public MrBallThread(SurfaceHolder surfaceHolder, Context context, Handler handler) {
            // get handles to some important objects
        	mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        	mSurfaceHolder = surfaceHolder;
            mContext = context;
            mHandler = handler;

            // Handle to our sprite
            mMrBallImage = context.getResources().getDrawable(R.drawable.mrball2);
            
            // load background image as a Bitmap instead of a Drawable b/c
            // we don't need to transform it and it's faster to draw this way
            mBackgroundImage = BitmapFactory.decodeResource(context.getResources(),R.drawable.background);
            
            mFloorImages[FLOOR_PIT] = BitmapFactory.decodeResource(context.getResources(),R.drawable.floor_pit);
            mFloorImages[FLOOR_SLOW] = BitmapFactory.decodeResource(context.getResources(),R.drawable.floor_slow);
            mFloorImages[FLOOR_NORMAL] = BitmapFactory.decodeResource(context.getResources(),R.drawable.floor_normal);
            mFloorImages[FLOOR_FAST] = BitmapFactory.decodeResource(context.getResources(),R.drawable.floor_fast);
 //           mFloorImages[FLOOR_PORTAL] = BitmapFactory.decodeResource(context.getResources(), R.drawable.floor_portal);
            
            // Initialize the rotation backlogs
            for(int i = 0; i < ORIENTATION_BACKLOG; i++)
            {
            	mXRotation[i] = 0.0;
            	mYRotation[i] = 0.0;
            }
        }

        /**
         * Starts the game, setting initial parameters
         */
        public void startGame() {
            synchronized (mSurfaceHolder) {

                // Initialize position and velocity
                mXPosition = BOARD_WIDTH/2;
                mYPosition = BOARD_HEIGHT/2;
                mXVelocity = 0;
                mYVelocity = 0;  
                
                thread.registerListener();
                
                // Not the place to actually do this, but for now....
                Random generator = new Random();
                for(int col = 0; col < FLOOR_COLS; col++){
                	for(int row = 0; row < FLOOR_ROWS; row++){
                		switch(generator.nextInt() % 4){
	                		case 0:  mFloors[col][row] = FLOOR_PIT; break;
	                		case 1:  mFloors[col][row] = FLOOR_SLOW; break;
	                		case 2:  mFloors[col][row] = FLOOR_NORMAL; break;
	                		case 3:  mFloors[col][row] = FLOOR_FAST; break;
	                		default: mFloors[col][row] = FLOOR_NORMAL; break;
                		}
                	}
                }
                
                // Place portals
//                mPortal1Col = 0;
//                mPortal1Row = 0;
//                mPortal2Col = FLOOR_COLS-1;
//                mPortal2Row = 0;
//                
//                mFloors[mPortal1Col][mPortal1Row] = FLOOR_PORTAL;
//                mFloors[mPortal2Col][mPortal2Row] = FLOOR_PORTAL;
                
                mLastTime = System.currentTimeMillis() + 100;
                setState(STATE_RUNNING);
            }
        }
        
        /**
         * Pauses the physics update & animation.
         */
        public void pause() {
            synchronized (mSurfaceHolder) {
                if (mMode == STATE_RUNNING) {
                	setState(STATE_PAUSE);
                	unregisterListener();
                }
            }
        }
        
        /**
         * Resumes from a pause.
         */
        public void unpause() {
            // Move the real time clock up to now
            synchronized (mSurfaceHolder) {
            	registerListener();
                mLastTime = System.currentTimeMillis() + 100;
            }
            setState(STATE_RUNNING);
        }
        
        public void tare(){
        	mXTare = getRotation(mXRotation);
        	mYTare = getRotation(mYRotation);
        	unpause();
        }

        @Override
        public void run() {
            while (mRun) {
                Canvas c = null;
                try {
                    c = mSurfaceHolder.lockCanvas(null);
                    synchronized (mSurfaceHolder) {
                        if (mMode == STATE_RUNNING) 
                        	updatePhysics();
                        doDraw(c);
                    }
                } finally {
                    // do this in a finally so that if an exception is thrown
                    // during the above, we don't leave the Surface in an
                    // inconsistent state
                    if (c != null) {
                        mSurfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
        }

        /**
         * Used to signal the thread whether it should be running or not.
         * Passing true allows the thread to run; passing false will shut it
         * down if it's already running. Calling start() after this was most
         * recently called with false will result in an immediate shutdown.
         * 
         * @param b true to run, false to shut down
         */
        public void setRunning(boolean b) {
            mRun = b;
        }

        /**
         * Sets the game mode. That is, whether we are running, paused, in the
         * failure state, in the victory state, etc.
         * 
         * @see #setState(int, CharSequence)
         * @param mode one of the STATE_* constants
         */
        public void setState(int mode) {
            synchronized (mSurfaceHolder) {
                setState(mode, null);
            }
        }

        /**
         * Sets the game mode. That is, whether we are running, paused, in the
         * failure state, in the victory state, etc.
         * 
         * @param mode one of the STATE_* constants
         * @param message string to add to screen or null
         */
        public void setState(int mode, CharSequence message) {
            /*
             * This method optionally can cause a text message to be displayed
             * to the user when the mode changes. Since the View that actually
             * renders that text is part of the main View hierarchy and not
             * owned by this thread, we can't touch the state of that View.
             * Instead we use a Message + Handler to relay commands to the main
             * thread, which updates the user-text View.
             */
            synchronized (mSurfaceHolder) {
                mMode = mode;

                if (mMode == STATE_RUNNING) {
                    Message msg = mHandler.obtainMessage();
                    Bundle b = new Bundle();
                    b.putString("text", "");
                    b.putInt("viz", View.INVISIBLE);
                    msg.setData(b);
                    mHandler.sendMessage(msg);
                } else {
                    Resources res = mContext.getResources();
                    CharSequence str = "";
                    if (mMode == STATE_READY)
                        str = res.getText(R.string.mode_ready);
                    else if (mMode == STATE_PAUSE)
                        str = res.getText(R.string.mode_pause);
                    else if (mMode == STATE_WIN)
                        str = res.getText(R.string.mode_win);
                   
                    if (message != null) {
                        str = message + "\n" + str;
                    }

                    Message msg = mHandler.obtainMessage();
                    Bundle b = new Bundle();
                    b.putString("text", str.toString());
                    b.putInt("viz", View.VISIBLE);
                    msg.setData(b);
                    mHandler.sendMessage(msg);
                }
            }
        }

        /* Callback invoked when the surface dimensions change. */
        public void setSurfaceSize(int width, int height) {
            // synchronized to make sure these all change atomically
            synchronized (mSurfaceHolder) {
                mScreenWidth = width;
                mScreenHeight = height;

                // don't forget to resize the background image
                mBackgroundImage = Bitmap.createScaledBitmap(mBackgroundImage, width, height, true);
                
                // Determine if the width or height is going to be the limiting dimension
                if((double)(width - MIN_SIDE_PANEL_WIDTH)/(double)height > (double)FLOOR_COLS/(double)FLOOR_ROWS)
                	mTileSize = (height)/FLOOR_ROWS;						// The screen is limited by height
                else
                	mTileSize = (width - MIN_SIDE_PANEL_WIDTH)/FLOOR_COLS;	// The screen is limited by width
                
                // Save the screen size of the ball
                mMrBallRadius = (int) (mTileSize * (BALL_RADIUS / TILE_SIZE ));
                
                // Set the dimensions of the board
                mBoardHeight = mTileSize * FLOOR_ROWS;
                mBoardWidth = mTileSize * FLOOR_COLS;
                
                // Set the size of the side panel
                mSidePanelWidth = mScreenWidth - mBoardWidth;
                
                // Set the offsets so that the board is centered appropriately
                mOffsetHeight = (mScreenHeight - mBoardHeight) / 2;
                mOffsetWidth = (mScreenWidth - mBoardWidth - mSidePanelWidth) / 2;
                
                // Scale all images to fit the new screen unit
                mFloorImages[FLOOR_PIT] = Bitmap.createScaledBitmap(mFloorImages[FLOOR_PIT], mTileSize, mTileSize, true);
                mFloorImages[FLOOR_SLOW] = Bitmap.createScaledBitmap(mFloorImages[FLOOR_SLOW], mTileSize, mTileSize, true);
                mFloorImages[FLOOR_NORMAL] = Bitmap.createScaledBitmap(mFloorImages[FLOOR_NORMAL], mTileSize, mTileSize, true);
                mFloorImages[FLOOR_FAST] = Bitmap.createScaledBitmap(mFloorImages[FLOOR_FAST], mTileSize, mTileSize, true);
       //         mFloorImages[FLOOR_PORTAL] = Bitmap.createScaledBitmap(mFloorImages[FLOOR_PORTAL], mTileSize, mTileSize, true);
            
                // Determine the model to screen conversion ratio
                mModelToScreenRatio = mTileSize/TILE_SIZE;
            }
        }

        /**
         * Draws the entire screen
         */
        private void doDraw(Canvas canvas) {
            // Draw the background image. Operations on the Canvas accumulate
            // so this is like clearing the screen.
            //canvas.drawBitmap(mBackgroundImage, 0, 0, null);
        	
        	 // Draw the side panel
            Paint backgroundColor = new Paint();
            backgroundColor.setARGB(255, 255, 0, 255);
            canvas.drawRect(0, 0, mScreenWidth, mScreenHeight, backgroundColor);
            
            drawFloor(canvas);
            
            // drawWalls(canvas);
            
            drawSidePanel(canvas); // This should really be done after drawing the ball, but ball isn't bounded yet
            
	        drawBall(canvas);
        }

        /**
         * Draws the floor
         */
        private void drawFloor(Canvas canvas) {
        	
            for(int col = 0; col < FLOOR_COLS; col++)
            {
            	for(int row = 0; row < FLOOR_ROWS; row++)
            	{
            		canvas.drawBitmap(mFloorImages[mFloors[col][row]], col*mTileSize + mOffsetWidth, row*mTileSize + mOffsetHeight, null);
            	}
            }
        }
        
        /**
         * Draws the floor
         */
        private void drawWalls(Canvas canvas) {
        	
        	// Draw horizontal walls
        	
        	// Draw vertical walls
        }
        
        /**
         * Draws the ball
         */
        private void drawBall(Canvas canvas) {
        	
            mMrBallImage.setBounds((int)((mXPosition * mModelToScreenRatio) - mMrBallRadius + mOffsetWidth),
            					   (int)((mYPosition * mModelToScreenRatio) - mMrBallRadius + mOffsetHeight), 
            					   (int)((mXPosition * mModelToScreenRatio) + mMrBallRadius + mOffsetWidth),
            					   (int)((mYPosition * mModelToScreenRatio) + mMrBallRadius + mOffsetHeight));
            mMrBallImage.draw(canvas);
        }
        
        /**
         * Draws the sidepanel
         */
        private void drawSidePanel(Canvas canvas) {
        	
        	// Draw the side panel
            Paint sidePanelColor = new Paint();
            sidePanelColor.setARGB(255, 0, 255, 255);
            canvas.drawRect(mScreenWidth - mSidePanelWidth, 0, mScreenWidth, mScreenHeight, sidePanelColor);
        }
        
        /**
         * Figures the ball state based on the passage of realtime.
         * Does not invalidate(). Called at the start of draw().
         * Detects the end-of-game and sets the UI to the next state.
         */
        private void updatePhysics() {
        	
        	// Get the current time
            long now = System.currentTimeMillis();

            // Do nothing if mLastTime is in the future.
            // This allows the game-start to delay the start of the physics
            // by 100ms or whatever.
            if (mLastTime > now) return;

            // Determine the time since last physics update
            double elapsed = (now - mLastTime) / 1000.0;
            
            // Save the old velocity
            double lOldXVelocity = mXVelocity;
            double lOldYVelocity = mYVelocity;

            // Get the current screen orientation
            double lXRotation = getRotation(mXRotation)*DEG_TO_RAD - mXTare;
            double lYRotation = getRotation(mYRotation)*DEG_TO_RAD - mYTare;
            
            // Calculate the new velocity
            mXVelocity -= Math.sin(lXRotation) * elapsed * GRAVITY * (1 - .2*Math.cos(lXRotation));
            mYVelocity += Math.sin(lYRotation) * elapsed * GRAVITY * (1 - .2*Math.cos(lYRotation));
            
            // Ensure X Velocity is with speed limit
            if(mXVelocity < -1*MAX_SPEED)
            	mXVelocity = -1*MAX_SPEED;
            else if(mXVelocity > MAX_SPEED)
            	mXVelocity = MAX_SPEED;
            
            // Ensure Y Velocity is with speed limit
            if(mYVelocity < -1*MAX_SPEED)
            	mYVelocity = -1*MAX_SPEED;
            else if(mYVelocity > MAX_SPEED)
            	mYVelocity = MAX_SPEED;
            
            // Move the ball
            // This will eventually require checking for collisions (increment, check, increment, check, ...)
            mXPosition += (mXVelocity + lOldXVelocity)/2 * elapsed;
            mYPosition += (mYVelocity + lOldYVelocity)/2 * elapsed;
            
            // Ensure the ball is not of the board (wrap around.. this is temporary)
            if(mXPosition < BALL_RADIUS){
            	mXPosition = BALL_RADIUS;
            	mXVelocity *= BOUNCE;
            }
            else if(mXPosition > BOARD_WIDTH - BALL_RADIUS){
            	mXPosition = BOARD_WIDTH - BALL_RADIUS;
            	mXVelocity *= BOUNCE;
            }
            if(mYPosition < BALL_RADIUS){
            	mYPosition = BALL_RADIUS;
            	mYVelocity *= BOUNCE;
            }
            else if(mYPosition > BOARD_HEIGHT - BALL_RADIUS){
            	mYPosition = BOARD_HEIGHT - BALL_RADIUS;
            	mYVelocity *= BOUNCE;
            }
            
            // Save the current update time
            mLastTime = now;
        }
        
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        	; // Do Nothing
        }
        
        public void onSensorChanged(SensorEvent event) {
        	        	
        	mXRotation[mRotationCounter] = event.values[1];
        	mYRotation[mRotationCounter] = event.values[2];
        	
        	mRotationCounter = (mRotationCounter + 1) % ORIENTATION_BACKLOG;
        }
        
        /**
         * Restores game state from the indicated Bundle. Typically called when
         * the Activity is being restored after having been previously
         * destroyed.
         * 
         * @param savedState Bundle containing the game state
         */
        public synchronized void restoreState(Bundle savedState) {
            synchronized (mSurfaceHolder) {
                setState(STATE_PAUSE);
               
                mXPosition = savedState.getDouble(KEY_X);
                mYPosition = savedState.getDouble(KEY_Y);
                mXVelocity = savedState.getDouble(KEY_DX);
                mYVelocity = savedState.getDouble(KEY_DY);
            }
        }
        
        /**
         * Dump game state to the provided Bundle. Typically called when the
         * Activity is being suspended.
         * 
         * @return Bundle with this view's state
         */
        public Bundle saveState(Bundle map) {
            synchronized (mSurfaceHolder) {
                if (map != null) {
                    map.putDouble(KEY_X, Double.valueOf(mXPosition));
                    map.putDouble(KEY_Y, Double.valueOf(mYPosition));
                    map.putDouble(KEY_DX, Double.valueOf(mXVelocity));
                    map.putDouble(KEY_DY, Double.valueOf(mYVelocity));
                }
            }
            return map;
        }
        
        private void registerListener(){
       	 mSensorManager.registerListener(this, 
        			mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION), 
        			SensorManager.SENSOR_DELAY_FASTEST);
       }
       
       private void unregisterListener(){
       		mSensorManager.unregisterListener(this);
       }
       
       private double getRotation(double[] rotation){
    	   
    	   double sum = 0;
    	   
    	   for (int i = 0; i < ORIENTATION_BACKLOG; i++)
    	   {
    		   sum += rotation[i];
    	   }
    	   
    	   return sum/ORIENTATION_BACKLOG;
       }
    }

    /** Handle to the application context, used to e.g. fetch Drawables. */
    private Context mContext;

    /** Pointer to the text view to display "Paused.." etc. */
    private TextView mStatusText;

    /** The thread that actually draws the animation */
    private MrBallThread thread;
    
    /** Handle to the saved instance state */
    private Bundle savedInstanceState;

    public MrBallView(Context context, AttributeSet attrs) {
        super(context, attrs);

        // register our interest in hearing about changes to our surface
        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
        
        mContext = context;

        setFocusable(true); // make sure we get key events
    }

    /**
     * Fetches the animation thread corresponding to this MrBallView.
     * 
     * @return the animation thread
     */
    public MrBallThread getThread() {
        return thread;
    }

    public void setSavedInstanceState(Bundle sis){
    	savedInstanceState = sis;
    }
    
    /**
     * Standard window-focus override. Notice focus lost so we can pause on
     * focus lost. e.g. user switches to take a call.
     */
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        if (!hasWindowFocus) 
        	thread.pause();
    }

    /**
     * Installs a pointer to the text view used for messages.
     */
    public void setTextView(TextView textView) {
        mStatusText = textView;
    }
    
   

    /* Callback invoked when the surface dimensions change. */
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        thread.setSurfaceSize(width, height);
        thread.startGame();
    }

    /*
     * Callback invoked when the Surface has been created and is ready to be
     * used.
     */
    public void surfaceCreated(SurfaceHolder holder) {
        // start the thread here so that we don't busy-wait in run()
        // waiting for the surface to be created
        thread = new MrBallThread(holder, mContext, new Handler() {
            @Override
            public void handleMessage(Message m) {
                mStatusText.setVisibility(m.getData().getInt("viz"));
                mStatusText.setText(m.getData().getString("text"));
            }
        });
        
        if (savedInstanceState == null) {
            // we were just launched: set up a new game
        	thread.setState(MrBallThread.STATE_READY);
        } else {
            // we are being restored: resume a previous game
        	thread.restoreState(savedInstanceState);
        	
        	// Clear the saved game so we don't restore it again
        	savedInstanceState = null;
        }
        
        thread.setRunning(true);
        thread.start();
    }

    /*
     * Callback invoked when the Surface has been destroyed and must no longer
     * be touched. WARNING: after this method returns, the Surface/Canvas must
     * never be touched again!
     */
    public void surfaceDestroyed(SurfaceHolder holder) {
        // we have to tell thread to shut down & wait for it to finish, or else
        // it might touch the Surface after we return and explode
        boolean retry = true;
        thread.unregisterListener();
        thread.setRunning(false);
        while (retry) {
            try {
                thread.join();
                retry = false;
            } catch (InterruptedException e) {
            }
        }
    }
}
